﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.IO;
using ExchangeHog.Pop3Client.Responses;
using ExchangeHog.Pop3Client.Commands;

namespace ExchangeHog.Pop3Client
{
	public sealed class Pop3Client : IDisposable
	{
		private const int DefaultPort = 110;

		private TcpClient client;
		private Stream streamClient;

		public event Action<string> Trace;
		private void OnTrace(string message) { if (Trace != null) Trace(message); }

		public string Hostname { get; private set; }
		public int Port { get; private set; }
		public bool UseSsl { get; private set; }
		public string Username { get; set; }
		public System.Security.SecureString Password { get; set; }
		public Pop3State CurrentState { get; private set; }

		public Pop3Client(string username, System.Security.SecureString password, string hostname, int port = DefaultPort, bool useSsl = false)
		{
			if (string.IsNullOrEmpty(hostname)) throw new ArgumentNullException("hostname");
			if (port < 0) throw new ArgumentOutOfRangeException("port");
			if (string.IsNullOrEmpty(username)) throw new ArgumentNullException("username");
			if (password == null) throw new ArgumentNullException("password");

			Hostname = hostname;
			Port = port;
			UseSsl = useSsl;
			Username = username;
			Password = password;
			client = new TcpClient();
			CurrentState = Pop3State.Unknown;
		}

		private void EnsureConnection()
		{
			if (!client.Connected)
				throw new Pop3Exception("Pop3 client is not connected.");
		}

		private void SetState(Pop3State state) { CurrentState = state; }

		private void EnsureResponse(Pop3Response response, String error = "")
		{
			if (response == null)
				throw new Pop3Exception("Unable to get Response.  Response object null.");

			if (!response.StatusIndicator)
				throw new Pop3Exception(string.IsNullOrEmpty(error)? response.HostMessage: (error + ": " + error));
		}

		private void TraceCommand<TCommand, TResponse>(TCommand command)
			where TCommand : Pop3Command<TResponse>
			where TResponse : Pop3Response
		{
			if (Trace != null)
				command.Trace += delegate(string message) { OnTrace(message); };
		}

		private void Connect()
		{
			//
			//	If a previous quit command was issued, the client would be disposed of.
			//
			if (client == null)
				client = new TcpClient();

			//
			//	if the connection already is established no need to reconnect.
			//
			if (!client.Connected)
			{
				SetState(Pop3State.Unknown);
				ConnectResponse response;
				using (ConnectCommand command = new ConnectCommand(client, Hostname, Port, UseSsl))
				{
					TraceCommand<ConnectCommand, ConnectResponse>(command);
					response = command.Execute(CurrentState);
					EnsureResponse(response);
				}

				SetClientStream(response.NetworkStream);

				SetState(Pop3State.Authorization);
			}
		}

		private void SetClientStream(Stream networkStream)
		{
			if (streamClient != null)
				streamClient.Dispose();
			streamClient = networkStream;
		}

		public void Authenticate()
		{
			Connect();

			using (UserCommand cmdUser = new UserCommand(streamClient, Username))
				ExecuteCommand<Pop3Response, UserCommand>(cmdUser);

			IntPtr ipPassword = System.Runtime.InteropServices.Marshal.SecureStringToBSTR(Password);
			string sPassword = System.Runtime.InteropServices.Marshal.PtrToStringBSTR(ipPassword);
			System.Runtime.InteropServices.Marshal.ZeroFreeBSTR(ipPassword);

			using (PassCommand cmdPass = new PassCommand(streamClient, sPassword))
				ExecuteCommand<Pop3Response, PassCommand>(cmdPass);

			CurrentState = Pop3State.Transaction;
		}

		public void Dele(Pop3ListItem item)
		{
			if (item == null)
				throw new ArgumentNullException("item");

			using (DeleCommand cmdDele = new DeleCommand(streamClient, item.MessageId))
				ExecuteCommand<Pop3Response, DeleCommand>(cmdDele);
		}

		public void Noop()
		{
			using (NoopCommand cmdNoop = new NoopCommand(streamClient))
				ExecuteCommand<Pop3Response, NoopCommand>(cmdNoop);
		}

		public void Rset()
		{
			using (RsetCommand cmdRset = new RsetCommand(streamClient))
				ExecuteCommand<Pop3Response, RsetCommand>(cmdRset);
		}

		public Stat Stat()
		{
			using (StatCommand cmdStat = new StatCommand(streamClient))
			{
				StatResponse response = ExecuteCommand<StatResponse, StatCommand>(cmdStat);
				return new Stat(response.MessageCount, response.Octets);
			}
		}

		public List<Pop3ListItem> List()
		{
			using (ListCommand cmdList = new ListCommand(streamClient))
				return ExecuteCommand<ListResponse, ListCommand>(cmdList).Items;
		}

		public Pop3ListItem List(int messageId)
		{
			using (ListCommand cmdList = new ListCommand(streamClient, messageId))
			{
				ListResponse response = ExecuteCommand<ListResponse, ListCommand>(cmdList);
				return new Pop3ListItem(response.MessageNumber, response.Octets);
			}
		}

		//public MimeEntity RetrMimeEntity(Pop3ListItem item)
		//{
		//    if (item == null)
		//        throw new ArgumentNullException("item");

		//    if (item.MessageId < 1)
		//        throw new ArgumentOutOfRangeException("item.MessageId");

		//    using (RetrCommand cmdRetr = new RetrCommand(streamClient, item.MessageId))
		//        return new MimeReader(ExecuteCommand<RetrResponse, RetrCommand>(cmdRetr).MessageLines).CreateMimeEntity();
		//}

		//public MessageParser Top(int messageId, int lineCount)
		//{
		//    if (messageId < 1)
		//        throw new ArgumentOutOfRangeException("messageId");

		//    if (lineCount < 0)
		//        throw new ArgumentOutOfRangeException("lineCount");

		//    using (TopCommand cmdTop = new TopCommand(streamClient, messageId, lineCount))
		//    {
		//        RetrResponse response = ExecuteCommand<RetrResponse, TopCommand>(cmdTop);
		//        MailMessageEx message = new MimeReader(response.MessageLines).CreateMimeEntity().ToMailMessageEx();
		//        message.Octets = response.Octets;
		//        message.MessageNumber = messageId;
		//        return message;
		//    }
		//}

		//public System.Net.Mail.MailMessage RetrieveMailMessage(Pop3ListItem item)
		//{
		//    MailMessageEx message = RetrMimeEntity(item).ToMailMessageEx();
		//    message.MessageNumber = item.MessageId;
		//    return message;
		//}

		public System.Net.Mail.MailMessage RetrieveMailMessage(Pop3ListItem item)
		{
			if (item == null)
				throw new ArgumentNullException("item");

			if (item.MessageId < 1)
				throw new ArgumentOutOfRangeException("item.MessageId");

			System.Net.Mail.MailMessage mm = null;
			using (RetrCommand cmdRetr = new RetrCommand(streamClient, item.MessageId))
			{
				MemoryStream ms = new MemoryStream();
				StreamWriter sw = new StreamWriter(ms, Encoding.ASCII);
				foreach (string s in ExecuteCommand<RetrResponse, RetrCommand>(cmdRetr).MessageLines)
					sw.WriteLine(s);
				sw.Flush();
				sw.Close();
				ExchangeHog.MessageParser.NewParser np = new MessageParser.NewParser();
				mm = np.ExtractMailMessage(ms);
			}
			return mm;
		}


		public void Quit()
		{
			using (QuitCommand cmdQuit = new QuitCommand(streamClient))
			{
				ExecuteCommand<Pop3Response, QuitCommand>(cmdQuit);

				//
				// Messages could have been deleted, reflect the server state.
				//
				if (CurrentState.Equals(Pop3State.Transaction))
					SetState(Pop3State.Update);

				Disconnect();

				//
				//	Quit command can only be called in Authorization or Transaction state, reset to Unknown.
				//
				SetState(Pop3State.Unknown);
			}
		}

		private TResponse ExecuteCommand<TResponse, TCommand>(TCommand command)
			where TResponse : Pop3Response
			where TCommand : Pop3Command<TResponse>
		{
			EnsureConnection();
			TraceCommand<TCommand, TResponse>(command);
			TResponse response = (TResponse)command.Execute(CurrentState);
			EnsureResponse(response);
			return response;
		}

		private void Disconnect()
		{
			//
			//	release underlying socket.
			//
			if (streamClient != null)
				streamClient.Close();

			if (client != null)
			{
				client.Close();
				client = null;
			}
		}

		public void Dispose() { Disconnect(); }
	}
}
